﻿using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Threading;
using Creventive.Wpf.ShellFactory.Logging;
using Creventive.Wpf.ShellFactory.Services;
using Creventive.Wpf.ShellFactory.Utility;

namespace Creventive.Wpf.ShellFactory.Modules
{
    /// <summary>
    ///   A ShellFactory extension to the Application base class (i.e. bootstrapper)
    /// </summary>
    public class ShellApplication : Application
    {
        private SealableCollection<ModuleReference> staticModules = new SealableCollection<ModuleReference>();

        private bool hasStarted;
        private ApplicationModule shellModule;
        private AssemblyName shellAssemblyName;

        /// <summary>
        ///   Gets or sets the shell module definition.
        /// </summary>
        /// <value>The shell module.</value>
        public ApplicationModule ShellModule
        {
            get { return this.shellModule; }
            set
            {
                if (this.hasStarted)
                    throw new InvalidOperationException("Cannot change shell module after the application has started.");
                this.shellModule = value;
            }
        }

        /// <summary>
        ///   Gets the static modules.
        /// </summary>
        /// <value>The static modules.</value>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public SealableCollection<ModuleReference> StaticModules
        {
            get { return this.staticModules; }
        }

        /// <summary>
        ///   Raises the <see cref = "E:System.Windows.Application.Startup" /> event.
        /// </summary>
        /// <param name = "e">A <see cref = "T:System.Windows.StartupEventArgs" /> that contains the event data.</param>
        protected override void OnStartup(StartupEventArgs e)
        {
            this.hasStarted = true;

            this.InitializeServices();

            // The logger is not available before the services has been initialized.
            Logger.Default.Info("Application Startup");

            this.InitializeErrorHandling();
            this.LoadModules();

            base.OnStartup(e);
        }

        private void InitializeErrorHandling()
        {
            // define application exception handler
            Current.DispatcherUnhandledException += this.Application_DispatcherUnhandledException;
        }

        private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            ServiceContainer.Default.GetService<IExceptionHandlingService>().ReportException(e.Dispatcher.Thread, e.Exception);
#if !DEBUG
            e.Handled = true;
#endif
        }

        private void LoadModules()
        {
            Logger.Default.Info("Loading Modules...");
            var moduleCatalog = (ApplicationModuleService)ServiceContainer.Default.GetService<IApplicationModuleService>();
            this.StaticModules.Seal();
            moduleCatalog.RegisterShellModule(this.shellAssemblyName, this.ShellModule);
            foreach (var moduleReference in this.StaticModules)
                moduleCatalog.RegisterStaticModule(moduleReference.Source);
            moduleCatalog.LoadModules();
        }

        private void InitializeServices()
        {
            this.shellAssemblyName = this.GetShellAssemblyName();
            if (this.shellAssemblyName == null)
                throw new NullReferenceException(@"GetShellAssemblyName cannot return null");

            var logger = this.CreateLoggerService();
            if (logger == null)
                throw new NullReferenceException(@"CreateLoggerService cannot return null");
            ServiceContainer.Default.RegisterService(logger, ServiceLifetime.ContainerLifetime);

            var exceptionHandler = this.CreateExceptionHandlingService();
            if (exceptionHandler == null)
                throw new NullReferenceException(@"CreateExceptionHandlingService cannot return null");
            ServiceContainer.Default.RegisterService(exceptionHandler, ServiceLifetime.ContainerLifetime);

            var moduleCatalog = this.CreateModuleCatalog();
            if (moduleCatalog == null)
                throw new NullReferenceException(@"CreateModuleCatalog cannot return null");
            ServiceContainer.Default.RegisterService<IApplicationModuleService>(moduleCatalog, ServiceLifetime.ContainerLifetime);
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Application.Exit"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.Windows.ExitEventArgs"/> that contains the event data.</param>
        protected override void OnExit(ExitEventArgs e)
        {
            base.OnExit(e);
            Logger.Default.Info("Application Shutdown (Exit Code = " + e.ApplicationExitCode.ToString("x8") + ")");
        }

        /// <summary>
        ///   Gets the name of the shell assembly.
        /// </summary>
        /// <returns></returns>
        protected virtual AssemblyName GetShellAssemblyName()
        {
            return this.GetType().Assembly.GetName();
        }

        /// <summary>
        ///   Creates the logger service.
        /// </summary>
        /// <returns></returns>
        protected virtual ILoggerService CreateLoggerService()
        {
            return new TraceLogger();
        }

        /// <summary>
        ///   Creates the module catalog.
        /// </summary>
        /// <returns></returns>
        protected virtual ApplicationModuleService CreateModuleCatalog()
        {
            return new ApplicationModuleService();
        }

        /// <summary>
        /// Creates an exception handling service for handling unexpected errors.
        /// </summary>
        /// <returns></returns>
        protected virtual IExceptionHandlingService CreateExceptionHandlingService()
        {
            return new ExceptionHandlingService();
        }
    }
}